﻿// Scalar.cs - Utilities for dealing with YAML scalars
//
// Copyright 2008-2011 by Burt Harris
// Licensed under the Microsoft Public License (Ms-PL)
// See: http://www.codeplex.com/yaml/license
//

using System;
using System.Diagnostics.Contracts;
using System.IO;

namespace YaTools.Yaml
{
    /// <summary>
    /// Utilities for dealing with scalar values
    /// </summary>
    public static class Scalar
    {
        #region Chomping enum

        public enum Chomping
        {
            None,
            Keep,
            Chomp
        };

        #endregion

        #region ScanFlags enum

        [Flags]
        public enum ScanFlags
        {
            None = 0,
            NonPrintable = 1,
            IndentedLines = 1 << 1,
            WiderThanRequested = 1 << 2,
            LeadingWhitespace = 1 << 3,
            Newlines = 1 << 4,
            SingleQuotes = 1 << 5,
            DoubleQuotes = 1 << 6,
            LeadingIndicator = 1 << 7,
            BlockIndicators = 1 << 8,
            NoTrailingNewline = 1 << 9,
            ManyTrailingNewlines = 1 << 10,
            FlowCollectionIndicators = 1 << 11,
            DocumentMarkup = 1 << 12,
            Chomp = 1 << 13,
            Keep = 1 << 14,
            Backslashes = 1 << 15
        }

        #endregion

        #region Style enum

        public enum Style
        {
            Default = 0,
            Plain,
            SingleQuoted,
            DoubleQuoted,
            Folded,
            Literal
        };

        #endregion

        public static bool IsNonSpace(char c)
        {
            int i = c;
            return (i > 0x20 && i < 0x7F) || (i >= 0xA0 && i < 0xD800)
                   || (i >= 0xE000 && i <= 0xFFFD);
        }

        public static bool IsPlainSafeIn(char c)
        {
            return IsNonSpace(c) && c != ',' && c != '[' && c != ']' &&
                   c != '{' && c != '}';
        }

        public static bool IsPlainSafeOut(char c)
        {
            return IsNonSpace(c);
        }

        public static bool IsPlainKey(string s, bool inFlow)
        {
            if (string.IsNullOrEmpty(s)) return false;
            if (s.Length > 1024) return false;

            char c = s[0]; // Check first character
            switch (c)
            {
                case ',':
                case '[':
                case ']':
                case '{':
                case '}':
                case '#':
                case '&':
                case '*':
                case '!':
                case '|':
                case '>':
                case '\'':
                case '"':
                case '%':
                case '@':
                case '`':
                case ' ':
                case '\t':
                    return false;

                case '?':
                case ':':
                case '-':
                    if (s.Length == 1 || !IsPlainSafeIn(s[1]))
                        return false;
                    break;

                default:
                    if (Char.IsControl(c)) return false;
                    break;
            }

            if (s.Contains(": ") || s.Contains(" #")) return false;

            if (inFlow && ((s.IndexOf(',') != -1) ||
                           (s.IndexOf('[') != -1) || (s.IndexOf(']') != -1) ||
                           (s.IndexOf('{') != -1) || (s.IndexOf('}') != -1))) return false;

            return true;
        }


        internal static void EmitScalar(TextWriter writer, Style style,
                                        int forceWidth, Chomping chomping,
                                        string str, bool flow,
                                        int indent)
        {
            Contract.Requires(writer != null);
            Contract.Requires(str != null);
            Contract.Requires(indent >= 0);

            if (string.IsNullOrEmpty(str))
            {
                writer.Write("\"\"");
                return;
            }

            Style favorStyle = flow ? Style.Plain : Style.Literal;

            DetermineStyle(ref style, flow, forceWidth, str, ref favorStyle);

            /* Write the text node */
            switch (style)
            {
                case Style.SingleQuoted:
                    WriteSingleQuoted(writer, forceWidth, str, indent);
                    break;

                case Style.DoubleQuoted:
                    WriteDoubleQuoted(writer, forceWidth, str, indent);
                    break;

                case Style.Folded:
                    WriteFolded(writer, forceWidth, chomping, str, indent);
                    break;

                case Style.Literal:
                    WriteLiteral(writer, chomping, str, indent);
                    break;

                case Style.Plain:
                    WritePlain(writer, forceWidth, str, indent);
                    break;
            }
        }

        public static void WriteSingleQuoted(
            TextWriter writer, int width, string value, int indentSpaces
            )
        {
            Contract.Requires(writer != null);
            Contract.Requires(value != null);
            Contract.Requires(indentSpaces >= 0);
            bool needIndent = false;
            int start = 0;
            int end = 0;
            writer.Write(Unicode.SingleQuote);

            for (int i = 0; i < value.Length; i++)
            {
                if (needIndent)
                {
                    writer.WriteSpaces(indentSpaces);
                    needIndent = false;
                }
                char ch = value[i];
                switch (ch)
                {
                    case Unicode.SingleQuote:
                        writer.Write("''");
                        break;

                    case Unicode.CR:
                    case Unicode.LF:
                        if (ch == Unicode.CR && i + 1 < value.Length && value[i + 1] == Unicode.LF)
                            i++;
                        end = i + 1;
                        if (start < value.Length && !Char.IsWhiteSpace(value[start])
                            && end < value.Length && !Char.IsWhiteSpace(value[end]))
                        {
                            writer.WriteLine();
                        }
                        needIndent = true;
                        start = i + 1;
                        break;

                    case Unicode.Space:
                        if (width > 0 &&
                            start < value.Length && value[start] != Unicode.Space &&
                            i - end > width)
                        {
                            needIndent = true;
                            end = i + 1;
                        }
                        else
                            writer.Write(Unicode.Space);
                        break;

                    default:
                        writer.Write(ch);
                        break;
                }
            }
            writer.Write(Unicode.SingleQuote);
        }

        public static void WriteDoubleQuoted(
            TextWriter writer, int width, string value, int indentSpaces)
        {
            Contract.Requires(writer != null);
            Contract.Requires(value != null);
            Contract.Requires(indentSpaces >= 0);

            writer.Write(Unicode.DoubleQuote);


            int doIndent = 0;
            int start = 0;
            int end = 0;

            for (int i = 0; i < value.Length; i++)
            {
                if (doIndent > 0)
                {
                    if (doIndent == 2)
                        writer.WriteLine('\\');
                    else
                        writer.WriteLine();
                    writer.WriteSpaces(indentSpaces);
                    doIndent = 0;
                }
                char ch = value[i];
                switch (ch)
                {
                        /* Escape sequences allowed within double quotes. */
                    case Unicode.DoubleQuote:
                        writer.Write("\\\"");
                        break;
                    case '\\':
                        writer.Write("\\\\");
                        break;
                    case '\0':
                        writer.Write("\\0");
                        break;
                    case '\a':
                        writer.Write("\\a");
                        break;
                    case '\b':
                        writer.Write("\\b");
                        break;
                    case '\f':
                        writer.Write("\\f");
                        break;
                        // case Ascii.CR: writer.Write("\\r"); break;
                    case '\t':
                        writer.Write("\\t");
                        break;
                    case '\v':
                        writer.Write("\\v");
                        break;
                    case Unicode.Escape:
                        writer.Write("\\e");
                        break;

                    case Unicode.CR:
                        writer.Write("\\r");
                        int j = i + 1;
                        if (j < value.Length && value[j] == Unicode.LF)
                            ch = value[i = j];
                        goto case Unicode.LF;

                    case Unicode.LF:
                        if (ch == Unicode.LF)
                            writer.Write("\\n");
                        end = i + 1;
                        start = i + 1;
                        doIndent = 2;
                        if (start < value.Length
                            && char.IsWhiteSpace(value[start]))
                        {
                            doIndent = 0;
                        }
                        break;

                    case Unicode.Space:
                        if (width > 0 && start < value.Length
                            && value[start] != Unicode.Space
                            && i - end > width)
                        {
                            doIndent = 1;
                            end = i + 1;
                        }
                        else
                            writer.Write(Unicode.Space);
                        break;

                    default:
                        if (!char.IsControl(ch) && ch <= (char) 0xFF)
                        {
                            writer.Write(ch);
                        }
                        else
                        {
                            if (ch <= 0xFF)
                                writer.Write("\\x{0:x2}", (int) ch);
                            else if (!Char.IsSurrogate(ch))
                                writer.Write("\\u{0:x4}", (int) ch);
                            else // First character of a surrogate
                            {
                                int k = i + 1;
                                if (!Char.IsLowSurrogate(ch)
                                    || k >= value.Length
                                    || !Char.IsHighSurrogate(value[k]))
                                {
                                    throw new ArgumentException(
                                        string.Format("Malformed Unicode Surrogate at index {0}", i), "value");
                                }
                                char ch2 = value[i = k];
                                writer.Write("\\U{0:x8}", Char.ConvertToUtf32(ch, ch2));
                            }
                        }
                        break;
                }
            }
            writer.Write(Unicode.DoubleQuote);
        }

        public static void WriteFolded(
            TextWriter writer, int width, Chomping chomping, string value, int indentSpaces
            )
        {
            Contract.Requires(writer != null);
            Contract.Requires(value != null);
            Contract.Requires(indentSpaces >= 0);

            int i, start = 0, next = 0;
            writer.Write('>');
            if (chomping == Chomping.Chomp)
                writer.Write('-');
            else if (chomping == Chomping.Keep)
                writer.Write('+');

            writer.IndentLine(indentSpaces);

            char[] buffer = value.ToCharArray();

            for (i = 0; i < buffer.Length; i++)
            {
                char ch = buffer[i];

                switch (ch)
                {
                    case Unicode.CR:
                    case Unicode.LF:
                        writer.Write(buffer, next, i - next);

                        // Special treatment for CRLF
                        if (ch == Unicode.CR
                            && i + 1 < buffer.Length
                            && buffer[i + 1] == Unicode.LF)
                            i++;
                        next = i + 1;

                        //	if (!Char.IsWhiteSpace(buffer[start]) 
                        // && !Char.IsWhiteSpace(buffer[next]))
                        writer.WriteLine();

                        if (i == buffer.Length)
                        {
                            if (chomping == Chomping.Keep)
                                writer.WriteLine();
                        }
                        else
                        {
                            writer.IndentLine(indentSpaces);
                        }

                        start = i + 1;
                        break;

                    case Unicode.Space:
                        if (buffer[start] != Unicode.Space)
                        {
                            if (width > 0 && i - next > width)
                            {
                                writer.Write(buffer, next, i - next);
                                writer.IndentLine(indentSpaces);
                                next = i + 1;
                            }
                        }
                        break;
                }
            }
            if (next < i)
            {
                writer.Write(buffer, next, i - next);
            }
        }

        public static void WritePlain(
            TextWriter writer, int width, string value, int indentSpaces
            )
        {
            Contract.Requires(writer != null);
            Contract.Requires(value != null);
            Contract.Requires(indentSpaces >= 0);

            int i, start = 0, next = 0;

            char[] buffer = value.ToCharArray();

            for (i = 0; i < buffer.Length; i++)
            {
                char ch = buffer[i];

                switch (ch)
                {
                    case Unicode.CR:
                    case Unicode.LF:
                        writer.Write(buffer, next, i - next);

                        // Special treatment for CRLF
                        if (ch == Unicode.CR
                            && i + 1 < buffer.Length
                            && buffer[i + 1] == Unicode.LF)
                            i++;
                        next = i + 1;

                        if (!Char.IsWhiteSpace(buffer[start])
                            && !Char.IsWhiteSpace(buffer[next]))
                            writer.WriteLine();

                        writer.IndentLine(indentSpaces);
                        start = i + 1;
                        break;

                    case Unicode.Space:
                        if (buffer[start] != Unicode.Space)
                        {
                            if (width > 0 && i - next > width)
                            {
                                writer.Write(buffer, next, i - next);
                                writer.IndentLine(indentSpaces);
                                next = i + 1;
                            }
                        }
                        break;
                }
            }
            if (next < i)
                writer.Write(buffer, next, i - next);
        }

        public static void WriteLiteral(
            TextWriter writer, Chomping chomping, string value, int indentSpaces
            )
        {
            Contract.Requires(writer != null);
            Contract.Requires(value != null);
            Contract.Requires(indentSpaces >= 0);

            char[] buffer = value.ToCharArray();

            int start = 0;
            int end;

            writer.Write("|");

            if (chomping == Chomping.Chomp) writer.Write("-");
            else if (chomping == Chomping.Keep) writer.Write("+");

            writer.IndentLine(indentSpaces);

            for (int i = 0; i < buffer.Length; i++)
            {
                char ch = buffer[i];
                switch (ch)
                {
                    case Unicode.CR:
                    case Unicode.LF:
                        // Mark end of line
                        end = i;

                        // Special treatment for CRLF
                        if (ch == Unicode.CR
                            && i + 1 < buffer.Length
                            && buffer[i + 1] == Unicode.LF)
                            i++;

                        if (!Char.IsWhiteSpace(buffer[start]) && !Char.IsWhiteSpace(buffer[end]))
                            end += 1;
                        writer.Write(buffer, start, end - start);

                        if (i + 1 == buffer.Length)
                        {
                            if (chomping != Chomping.Keep)
                                writer.WriteLine();
                        }
                        else
                            writer.IndentLine(indentSpaces);
                        start = i + 1;
                        break;
                }
            }
            end = buffer.Length;
            if (start < end)
                writer.Write(buffer, start, end - start);
        }

        public static ScanFlags DetermineStyle(
            ref Style forceStyle, bool flow, int forceWidth,
            string str, ref Style favorStyle
            )
        {
            Contract.Requires(str != null);
            Style scalarStyle = Style.Default;

            ScanFlags scan = Scan(forceWidth, str);

            /* if still arbitrary, sniff a good block style. */
            if (forceStyle == Style.Default)
            {
                if ((scan & ScanFlags.Newlines) != 0 && !flow)
                {
                    forceStyle = Style.Literal;
                }
                else
                {
                    forceStyle = Style.Plain;
                }
            }

            if (scalarStyle == Style.Folded)
            {
                favorStyle = Style.Folded;
            }

            /* Determine block style */
            if ((scan & ScanFlags.NonPrintable) != 0)
            {
                forceStyle = Style.DoubleQuoted;
            }
            else if ((scan & ScanFlags.LeadingWhitespace) != 0)
            {
                forceStyle = Style.DoubleQuoted;
            }
            else if (forceStyle != Style.Folded && (scan & ScanFlags.IndentedLines) != 0)
            {
                forceStyle = Style.Literal;
            }
            else if (forceStyle == Style.Plain && (scan & ScanFlags.Newlines) != 0)
            {
                forceStyle = favorStyle;
            }
            else if (forceStyle == Style.Plain && flow && (scan & ScanFlags.FlowCollectionIndicators) != 0)
            {
                forceStyle = Style.DoubleQuoted;
            }
                /* Commened out in Syck - Produces ugly output...
             * else if (force_style == ScalarStyle.fold && ((scan & ScanFlags.WIDE) == 0))
            {
                force_style = ScalarStyle.literal;
            } */
            else if (forceStyle == Style.Plain &&
                     ((scan & (ScanFlags.LeadingIndicator | ScanFlags.BlockIndicators)) != 0))
            {
                forceStyle = (scan & ScanFlags.Newlines) != 0 ? favorStyle : Style.DoubleQuoted;
            }

            if (forceStyle == Style.DoubleQuoted
                && ((scan & (ScanFlags.Backslashes | ScanFlags.DoubleQuotes)) != 0)
                && (scan & (ScanFlags.NonPrintable | ScanFlags.Newlines)) == 0)
            {
                forceStyle = Style.SingleQuoted;
            }


            // TODO: What goes here?

            /* Fix the ending newlines */
            if ((scan & ScanFlags.NoTrailingNewline) != 0)
            {
                scan |= ScanFlags.Chomp;
            }
            else if ((scan & ScanFlags.ManyTrailingNewlines) != 0)
            {
                scan |= ScanFlags.Keep;
            }

            return scan;
        }

        private static bool IsBreak(char c)
        {
            switch (c)
            {
                case Unicode.CR:
                case Unicode.LF:
                    return true;
            }
            return false;
        }

        public static ScanFlags Scan(int requestedLength, string str)
        {
            Contract.Requires(str != null);

            ScanFlags flags = ScanFlags.None;
            int lineStart = 0;

            if (str.Length > 0)
            {
                // Check the first character or two to see if it starts with an indicator
                switch (str[0])
                {
                    case '[':
                    case ']':
                    case '{':
                    case '}':
                    case ',':
                    case '!':
                    case '*':
                    case '&':
                    case '|':
                    case '>':
                    case '#':
                    case '%':
                    case '@':
                    case Unicode.SingleQuote:
                    case Unicode.DoubleQuote:
                        flags |= ScanFlags.LeadingIndicator;
                        break;

                    case '-':
                    case ':':
                    case '?':
                        if (str.Length > 1 && str[1] == Unicode.Space)
                            flags |= ScanFlags.LeadingIndicator;
                        break;


                    case Unicode.Space:
                    case '\t':
                        flags |= ScanFlags.LeadingWhitespace;
                        break;
                }

                // Check on trailing line breaks
                int i = str.Length - 1;
                if (!IsBreak(str[i]))
                {
                    flags |= ScanFlags.NoTrailingNewline;
                }
                else
                {
                    flags |= ScanFlags.Newlines;
                    if (str[i] == Unicode.LF && i > 0 && str[i - 1] == Unicode.CR)
                        i--;
                    if (i > 0 && IsBreak(str[i - 1]))
                        flags |= ScanFlags.ManyTrailingNewlines;
                }

                if (str.StartsWith("---"))
                    flags |= ScanFlags.DocumentMarkup;

                for (i = 0; i < str.Length; i++)
                {
                    char ch = str[i];
                    switch (ch)
                    {
                        case Unicode.SingleQuote:
                            flags |= ScanFlags.SingleQuotes;
                            break;

                        case Unicode.DoubleQuote:
                            flags |= ScanFlags.DoubleQuotes;
                            break;

                        case '\\':
                            flags |= ScanFlags.Backslashes;
                            break;

                        case '[':
                        case ']':
                        case '{':
                        case '}':
                        case ',':
                            flags |= ScanFlags.FlowCollectionIndicators;
                            break;

                        case '-':
                        case ':':
                        case '?':
                            if (i < str.Length - 2 && str[i + 1] == Unicode.Space)
                                flags |= ScanFlags.BlockIndicators;
                            break;

                        case Unicode.CR:
                        case Unicode.LF:
                            flags |= ScanFlags.Newlines;

                            if (requestedLength > 0 && i - lineStart > requestedLength)
                                flags |= ScanFlags.WiderThanRequested;

                            if (i < str.Length - 2)
                            {
                                switch (str[i + 1])
                                {
                                    case Unicode.Space:
                                    case '\t':
                                        flags |= ScanFlags.IndentedLines;
                                        break;
                                }
                                int j = i + 1;
                                if (j < str.Length)
                                {
                                    char ch2 = str[j];
                                    if (ch2 == '%')
                                        flags |= ScanFlags.DocumentMarkup;
                                    else if (ch2 == '-' && str.IndexOf("---", j, 1) == 0)
                                        flags |= ScanFlags.DocumentMarkup;
                                    else if (ch2 == '.' && str.IndexOf("...", j, 1) == 0)
                                        flags |= ScanFlags.DocumentMarkup;
                                }
                            }
                            break;

                        default:
                            if (Char.IsControl(ch))
                                flags |= ScanFlags.NonPrintable;
                            break;
                    }
                }
                if (requestedLength > 0 && i - lineStart > requestedLength)
                    flags |= ScanFlags.WiderThanRequested;
            }
            return flags;
        }
    }
}